Skip to content

feature(repo_cleanup): make branch deletion reachable from a cloud session - #278

Merged
Jammy2211 merged 2 commits into
mainfrom
claude/repo-cleanup-82k6kh
Aug 25, 2026
Merged

feature(repo_cleanup): make branch deletion reachable from a cloud session#278
Jammy2211 merged 2 commits into
mainfrom
claude/repo-cleanup-82k6kh

Conversation

@Jammy2211

@Jammy2211 Jammy2211 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Why

A cloud Claude session — phone, or claude.ai/code — can audit branches but cannot remove one. git push origin --delete returns 403 for the session credential (the agent proxy logs no relay failure, so this is GitHub's policy, not egress), and the GitHub tool surface those sessions get has no delete-ref call at all. Branch cleanup was therefore laptop-only.

It showed. A /repo_cleanup sweep on 2026-08-25 found 233 branches across Mind and Brain, 188 of them provably spent, and could act on none of it.

A workflow's GITHUB_TOKEN is a different credential, and these repos already trust it with contents: writedashboard_refresh.yml commits to main with it. So the sweep now runs inside the repo on Actions, and any surface that can dispatch a workflow can drive it.

What's here

  • bin/branch_sweep.sh — the executable half of the skill. Classifies every remote branch via branch_contribution.sh (never a hand-rolled ahead-count, per docs/agent_failure_modes.md D1/D2) and deletes only what is provably contained. Two pieces of care, both earned during the audit that motivated this:
    • It unshallows first. On a truncated history every ancestry question is wrong in the same direction — nothing looks contained — so a shallow clone would sweep nothing while reporting success. Not hypothetical: the motivating audit ran shallow first and read 94 plainly-merged branches as unmerged.
    • A CONTRIBUTES branch may still be a squash-merge git 2.34 cannot see through. It asks GitHub for a merged PR at that exact tip, then falls back to proving the squash locally by patch-id. Unproven means kept. The API path is what recovers the 6 of PyAutoMind's 19 squash-merges the local proof declines.
  • .github/workflows/branch_sweep.yml — dispatch (audit|delete, optional limit) plus a weekly audit-only cron. A scheduled run forces audit regardless of input, so nothing is ever removed unattended. The step summary carries the report, which is the readable surface on a phone.
  • tests/test_branch_sweep.py — known-answer repos for the four gates that keep a branch out of the delete set: main, archive/condemned/* Gut transit refs, open PR heads, and unproven content. The deletions are one git push; the refusals are the part worth pinning. One test performs a real delete against a live origin and asserts every gate held.
  • skills/repo_cleanup/ — routes cloud sessions to the dispatch instead of reporting the sweep blocked, and says once that a repo still accumulating merged heads has "Automatically delete head branches" off — the setting that stops this backlog forming, which no sweep substitutes for.

Checks

pytest tests/499 passed. bin/check_skill_line_counts.sh → OK (all 39 skills within the 200-line budget).

Merge order

This PR merges before PyAutoLabs/PyAutoMind#321. That workflow checks PyAutoBrain out at its default branch and runs .brain/bin/branch_sweep.sh; until this lands, the script is not on main and a dispatch there would fail.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq

…ssion

A cloud Claude session — phone or claude.ai/code — can audit branches but
cannot remove one: `git push origin --delete` returns 403 for the session
credential (the proxy logs no relay failure, so it is GitHub's policy, not
egress), and the GitHub tool surface those sessions get has no delete-ref
call at all. Branch cleanup was therefore laptop-only, and it showed: a sweep
on 2026-08-25 found 233 branches across Mind and Brain, 188 of them provably
spent, with nothing able to act on the finding.

A workflow's GITHUB_TOKEN is a different credential, and these repos already
trust it with `contents: write` — dashboard_refresh.yml commits to main with
it. So the sweep now runs inside the repo on Actions, and any surface that can
dispatch a workflow can drive it.

- `bin/branch_sweep.sh` — the executable half of the skill. Classifies every
  remote branch via branch_contribution.sh (never a hand-rolled ahead-count,
  per agent_failure_modes D1/D2) and deletes only what is provably contained.
  Two extra pieces of care:
    * It unshallows first. On a truncated history every ancestry question is
      wrong in the same direction — nothing looks contained — so a shallow
      clone would silently sweep nothing while reporting success. This was not
      hypothetical: the audit that motivated this ran shallow first and read 94
      plainly-merged branches as unmerged.
    * A CONTRIBUTES branch may still be a squash-merge git 2.34 cannot see
      through. It asks GitHub for a merged PR at that exact tip, then falls
      back to proving the squash locally by patch-id. Unproven means kept.
- `.github/workflows/branch_sweep.yml` — dispatch (audit|delete, optional
  limit) plus a weekly audit-only cron; a scheduled run cannot delete whatever
  the cron says. The step summary carries the report, which is the readable
  surface on a phone.
- `tests/test_branch_sweep.py` — known-answer repos for the four gates that
  keep a branch out of the delete set: main, `archive/condemned/*` Gut transit
  refs, open PR heads, and unproven content. The deletions are one push; the
  refusals are the part worth pinning.
- The skill now routes cloud sessions to the dispatch instead of telling them
  the sweep is blocked, and says once that a repo still accumulating merged
  heads has "Automatically delete head branches" off — the setting that stops
  this backlog forming, which no sweep substitutes for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq
CI caught two things the local run could not.

1. The shallow guard tested `-f "$(git -C $REPO rev-parse --git-dir)/shallow"`.
   With `-C`, `--git-dir` comes back *relative* (`.git`), so that path resolves
   against the calling process's cwd — the guard was really asking "is the
   directory I happen to be standing in a shallow clone?". On an Actions runner
   the answer is yes, because actions/checkout is shallow by default, so the
   sweep tried to unshallow a complete repository and died on
   `fatal: --unshallow on a complete repository does not make sense`.

   Now it asks `rev-parse --is-shallow-repository`, the purpose-built predicate,
   which is about the repo named by `-C` and nothing else. Reproduced first: a
   cwd carrying `.git/shallow` makes the old expression true and the new one
   false for the same complete repo.

   Worth noting the failure mode was the safe direction by luck, not design —
   it aborted rather than sweeping on bad verdicts. The next caller might not
   be so lucky.

2. Prerequisite ordering. The gh check ran *after* the fetch/unshallow, so a
   run with no gh rewrote the caller's clone and only then refused. Moved both
   the presence and auth checks above any git mutation: if we are going to say
   no, say it before touching anything.

The test's deliberately-lean PATH gained the coreutils the script actually
calls (dirname and friends); without them the no-gh test was exercising a
missing-dirname crash rather than the refusal it claims to pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq
@Jammy2211
Jammy2211 merged commit 248331c into main Aug 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants